import framebuf
from machine import Pin
import time

# ILI9163C Command Definitions
ILI9163C_SWRESET = 0x01  # Software Reset
ILI9163C_SLPOUT = 0x11   # Sleep Out
ILI9163C_COLMOD = 0x3A   # Color Mode
ILI9163C_GAMSET = 0x26   # Gamma Set
ILI9163C_DISPON = 0x29   # Display ON
ILI9163C_CASET = 0x2A    # Column Address Set
ILI9163C_RASET = 0x2B    # Row Address Set
ILI9163C_RAMWR = 0x2C    # RAM Write
ILI9163C_MADCTL = 0x36   # Memory Access Control
ILI9163C_INVCOL = 0x21   # Display Inversion On/Off
ILI9163C_GMCTRP1 = 0xE0  # Positive Gamma Correction
ILI9163C_GMCTRN1 = 0xE1  # Negative Gamma Correction
ILI9163C_FRMCTR1 = 0xB1  # Frame Rate Control (Normal Mode)
ILI9163C_INVCTR = 0xB4   # Display Inversion Control
ILI9163C_PWCTR1 = 0xC0   # Power Control 1
ILI9163C_PWCTR2 = 0xC1   # Power Control 2
ILI9163C_VMCTR1 = 0xC5   # VCOM Control 1
ILI9163C_VMCTR2 = 0xC7   # VCOM Control 2 (Offset)

# Color Definitions (RGB565 format)
BLACK = 0x0000
WHITE = 0xFFFF

# Inherit from framebuf.FrameBuffer
class ILI9163C_SoftSPI(framebuf.FrameBuffer):
    def __init__(self, scl_pin, sda_pin, cs_pin, rst_pin=None, width=128, height=128, rot=0, bgr=False):
        """
        MicroPython driver for ILI9163C LCD using software (bit-banged) 3-wire SPI.
        This version inherits from framebuf.FrameBuffer, allowing direct drawing.
        The D/C bit is embedded as the MSB of a 9-bit transfer.

        :param scl_pin: GPIO pin number for Serial Clock (SCL)
        :param sda_pin: GPIO pin number for Serial Data (SDA)
        :param cs_pin: GPIO pin number for Chip Select (CS)
        :param rst_pin: Optional GPIO pin number for Reset (RST)
        :param width: Display width in pixels (default 128)
        :param height: Display height in pixels (default 128)
        :param rot: Display rotation (0, 1, 2, 3)
        :param bgr: True for BGR color order, False for RGB (default False)
        """
        self.width = width
        self.height = height
        self.rot = rot
        self.bgr = bgr

        # Initialize GPIO pins
        self.scl = Pin(scl_pin, Pin.OUT)
        self.sda = Pin(sda_pin, Pin.OUT)
        self.cs = Pin(cs_pin, Pin.OUT)
        self.rst = Pin(rst_pin, Pin.OUT) if rst_pin else None

        # Set initial pin states
        self.scl(0) # Clock starts low (SPI Mode 0)
        self.cs(1)  # CS high (inactive)
        if self.rst:
            self.rst(1) # RST high (not in reset)

        # Create a FrameBuffer for drawing operations
        self.buffer = bytearray(self.width * self.height * 2) # 2 bytes per pixel for RGB565
        
        # Initialize the FrameBuffer superclass
        super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)

        self.reset()
        self._init_display()

        # Clear display after initialization
        self.fill(BLACK) # Now calls the inherited fill method
        self.show()

    def _write_bit(self, bit):
        """Sends a single bit over the SDA line, toggling SCL."""
        self.scl(0)         # Clock low
        self.sda(bit)       # Set data bit
        # time.sleep_us(1)  # Optional: Add small delay if display needs slower clock
        self.scl(1)         # Clock high (data latched on rising edge)
        # time.sleep_us(1)  # Optional: Add small delay
        self.scl(0)         # Clock low for next cycle

    def _write_9bit_packet(self, value, is_data):
        """
        Sends a 9-bit packet (D/C + 8-bit value) using software SPI.
        MSB (D/C bit) is sent first, then the 8-bit value MSB first.
        """
        self.cs(0) # Assert CS (pull low) for the transaction

        # Send D/C bit (MSB of the 9-bit packet)
        dc_bit = 1 if is_data else 0
        self._write_bit(dc_bit)

        # Send the 8-bit value, MSB first
        for i in range(7, -1, -1): # Iterate from bit 7 down to 0
            bit = (value >> i) & 0x01
            self._write_bit(bit)

        self.cs(1) # De-assert CS (pull high) after the packet

    def _write_cmd(self, cmd):
        """Sends a command byte to the display."""
        self._write_9bit_packet(cmd, is_data=False)

    def _write_data(self, data):
        """Sends a data byte or bytearray to the display."""
        if isinstance(data, int):
            self._write_9bit_packet(data, is_data=True)
        elif isinstance(data, (bytes, bytearray)):
            self.cs(0) # Assert CS for multiple data transfers
            for byte in data:
                # For each byte of data, prepend the D/C bit (1)
                self._write_bit(1) # D/C = 1 for data
                # Send the 8-bit data byte
                for i in range(7, -1, -1):
                    bit = (byte >> i) & 0x01
                    self._write_bit(bit)
            self.cs(1) # De-assert CS after all data is complete
        else:
            raise ValueError("Data must be an int, bytes, or bytearray")

    def reset(self):
        """Performs a hardware reset on the display."""
        if self.rst:
            self.rst(1)
            time.sleep_ms(50)
            self.rst(0)
            time.sleep_ms(50)
            self.rst(1)
        else:
            # If no RST pin, try a software reset
            self._write_cmd(ILI9163C_SWRESET)

        time.sleep_ms(150)

    def _init_display(self):
        """Initializes the display with a common sequence of commands."""
        self._write_cmd(ILI9163C_SWRESET) # Software reset
        time.sleep_ms(150)

        self._write_cmd(ILI9163C_SLPOUT) # Exit sleep
        time.sleep_ms(150)

        # --- Power Control settings (common for ILI9163C) ---
        self._write_cmd(ILI9163C_PWCTR1) # Power Control 1
        self._write_data(0x06) # GVDD = 4.7V, DDVDH = VCIx2
        self._write_cmd(ILI9163C_PWCTR2) # Power Control 2
        self._write_data(0x01) # VGH/VGL

        self._write_cmd(ILI9163C_VMCTR1) # VCOM Control 1
        self._write_data(0x30) # VCOM = 0.92V
        self._write_cmd(ILI9163C_VMCTR2) # VCOM Control 2
        self._write_data(0xC1) # VCOM Offset

        # --- Frame Rate Control ---
        self._write_cmd(ILI9163C_FRMCTR1) # Frame Rate Control (Normal Mode)
        self._write_data(0x05) # 105Hz
        self._write_cmd(ILI9163C_INVCTR) # Display Inversion Control
        self._write_data(0x03) # Column + Row inversion

        # --- Memory Access Control (MADCTL) ---
        madctl = 0x00
        if self.rot == 0:
            madctl = 0x08 # MY=0 MX=0 MV=0 ML=0 RGB=1 (Normal)
        elif self.rot == 1:
            madctl = 0xA8 # MY=1 MX=0 MV=1 ML=0 RGB=1 (Rotate 90)
        elif self.rot == 2:
            madctl = 0xC8 # MY=1 MX=1 MV=0 ML=0 RGB=1 (Rotate 180)
        elif self.rot == 3:
            madctl = 0x68 # MY=0 MX=1 MV=1 ML=0 RGB=1 (Rotate 270)

        if self.bgr:
            madctl |= 0x08 # BGR Bit (bit 3) if needed for your specific panel
            # Note: For ILI9163C, the BGR bit is typically MADCTL[3]. Some panels
            # might require 0x08 for RGB and 0x00 for BGR, or vice-versa.
            # You might need to experiment if colors are off.

        self._write_cmd(ILI9163C_MADCTL)
        self._write_data(madctl)

        # --- Color Mode ---
        self._write_cmd(ILI9163C_COLMOD)
        self._write_data(0x05) # 16-bit color (RGB565)

        # --- Gamma Set ---
        self._write_cmd(ILI9163C_GAMSET)
        self._write_data(0x01) # Gamma Curve 1 (Default)

        # --- Positive Gamma Correction ---
        self._write_cmd(ILI9163C_GMCTRP1)
        self._write_data(bytearray([0x3F, 0x25, 0x1C, 0x1E, 0x20, 0x12, 0x2A, 0x90, 0x08, 0x18, 0x0F, 0x11, 0x02, 0x02, 0x00]))

        # --- Negative Gamma Correction ---
        self._write_cmd(ILI9163C_GMCTRN1)
        self._write_data(bytearray([0x00, 0x18, 0x15, 0x02, 0x0F, 0x04, 0x0C, 0x36, 0x03, 0x06, 0x0B, 0x05, 0x09, 0x0F, 0x07]))

        self._write_cmd(ILI9163C_DISPON) # Display On
        time.sleep_ms(100)

    def _set_window(self, x0, y0, x1, y1):
        """
        Sets the active window for pixel operations.
        Note: Some 128x128 ILI9163C panels might have a small hardware offset
        (e.g., 2 pixels) for X and Y coordinates. Adjust x_offset/y_offset if needed.
        """
        x_offset = 0 # Adjust if your display has an X offset
        y_offset = 0 # Adjust if your display has an Y offset

        self._write_cmd(ILI9163C_CASET)  # Column address set
        self._write_data(bytearray([(x0 + x_offset) >> 8, (x0 + x_offset) & 0xFF]))
        self._write_data(bytearray([(x1 + x_offset) >> 8, (x1 + x_offset) & 0xFF]))

        self._write_cmd(ILI9163C_RASET)  # Row address set
        self._write_data(bytearray([(y0 + y_offset) >> 8, (y0 + y_offset) & 0xFF]))
        self._write_data(bytearray([(y1 + y_offset) >> 8, (y1 + y_offset) & 0xFF]))

        self._write_cmd(ILI9163C_RAMWR)  # Memory write (prepares for pixel data)

    def show(self):
        """Pushes the framebuffer content to the display."""
        self._set_window(0, 0, self.width - 1, self.height - 1)
        
        self.cs(0) # Assert CS for the entire framebuffer data transfer

        # Iterate through the buffer (RGB565, 2 bytes per pixel)
        for i in range(len(self.buffer)):
            byte = self.buffer[i]
            # Each byte is display data, so D/C bit is 1
            self._write_bit(1) # D/C = 1 for data
            # Send the 8-bit data byte
            for j in range(7, -1, -1):
                bit = (byte >> j) & 0x01
                self._write_bit(bit)
        
        self.cs(1) # De-assert CS after transfer is complete

    @staticmethod
    def rgb565(r, g, b, little_endian=True):
        color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
        return (color & 0xFF) << 8 | (color >> 8) if little_endian else color        

# The fill, pixel, text, rect, fill_rect, line, and blit methods
# are now inherited directly from framebuf.FrameBuffer.
